/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.time;

import ore.errors.OrekitException;

import java.util.Map;
import java.util.SortedMap;

/** 
 * Coordinated Universal Time.
 * 
 * UTC is related to TAI using step adjustments from time to time
 * according to IERS (International Earth Rotation Service) rules. These
 * adjustments require introduction of leap seconds.
 * 
 * Leap seconds are always inserted as additional seconds at the last minute
 * of the day, pushing the next day forward. Such minutes are therefore more
 * than 60 seconds long. 
 * 
 * For example, when a one second leap was introduced at the end of
 * 2005, the UTC time sequence was 2005-12-31T23:59:59 UTC, followed
 * by 2005-12-31T23:59:60 UTC, followed by 2006-01-01T00:00:00 UTC.
 * 
 * This class intended to be accessed via the {@link TimeScalesFactory} class.
 * 
 * @author Luc Maisonobe
 * @see AbsoluteDate
 */
public final class UTCScale
    extends Object
    implements TimeScale
{
    private volatile static TimeScale Instance;
    /** 
     * @exception OrekitException Time steps cannot be read
     */
    public final static TimeScale Instance()
        throws OrekitException
    {
        TimeScale instance = Instance;
        if (null == instance)
            return (Instance = new UTCScale());
        else
            return instance;
    }


    /** 
     * Reference TAI date.
     */
    private static final AbsoluteDate TAI_REFERENCE =
        new AbsoluteDate(DateComponents.J2000_EPOCH, TimeComponents.H12, TimeScalesFactory.getTAI());


    /** 
     * Time steps.
     */
    private UTCTAIOffset[] offsets;
    /**
     * As time scale references tend to be linearly regional, this
     * field records the last used index as a starting point for the
     * next used index -- as an optimization in the offsets array
     * scannning.
     * 
     * This field is not marked volatile, because cpu caching is an
     * optimization of the intended purpose of this field value.
     */
    private int last;


    /** 
     * @exception OrekitException if the time steps cannot be read
     */
    private UTCScale() throws OrekitException {
        super();

        UTCTAIHistory entries = UTCTAIHistory.Instance();

        this.offsets = new UTCTAIOffset[entries.size() + 1];
        int current = 0;
        /*
         * Set up a first entry covering the far past before first
         * leap second
         */
        UTCTAIOffset last = new UTCTAIOffset(AbsoluteDate.PAST_INFINITY, 0, 0);
        this.offsets[current++] = last;
        /*
         * Add leap second entries in chronological order
         */
        for (Map.Entry<DateComponents, Integer> entry : entries.entrySet()) {
            final double offset            = entry.getValue().doubleValue();
            final double leap              = offset - last.getOffset();
            final AbsoluteDate taiDayStart = new AbsoluteDate(entry.getKey(), TimeScalesFactory.getTAI());
            final AbsoluteDate leapDate    = new AbsoluteDate(taiDayStart, last.getOffset());
            last.setValidityEnd(leapDate);
            last = new UTCTAIOffset(leapDate, leap, offset);
            this.offsets[current++] = last;
        }
        /*
         * Reference to last known leap
         */
        this.last = (current - 1);
    }


    public double offsetFromTAI(AbsoluteDate date) {
        int index = this.indexOf(date);
        return -(offsets[index].getOffset());
    }
    public double offsetToTAI(DateComponents date, TimeComponents time) {
        int index = this.indexOf(date.getJ2000Day() * 86400.0 + time.getSecondsInDay() - 43200);
        return offsets[index].getOffset();
    }
    public String getName() {
        return "UTC";
    }
    public String toString() {
        return this.getName();
    }
    /** 
     * @return Date of the first known leap second
     */
    public AbsoluteDate getFirstKnownLeapSecond() {
        return this.offsets[0].getValidityEnd();
    }
    /** 
     * @return Date of the last known leap second
     */
    public AbsoluteDate getLastKnownLeapSecond() {
        return this.offsets[offsets.length - 1].getDate();
    }
    /** 
     * @param date Date to check
     * @return Date is within a leap second introduction
     */
    public boolean insideLeap(AbsoluteDate date) {
        int index = this.indexOf(date);
        return (0 > date.compareTo(offsets[index].getValidityStart()));
    }
    /** 
     * @param date Date to check
     * @return Value of the previous leap
     */
    public double getLeap(AbsoluteDate date) {
        int index = this.indexOf(date);
        return this.offsets[index].getLeap();
    }

    private int indexOf(AbsoluteDate date) {
        int current = this.last;
        while (date.compareTo(offsets[current].getValidityStart()) < 0) {
            --current;
        }
        while (date.compareTo(offsets[current].getValidityEnd()) >= 0) {
            ++current;
        }
        this.last = current;
        return current;
    }
    private int indexOf(double utcTime) {
        int current = this.last;
        while (offsets[current].getValidityStart().durationFrom(TAI_REFERENCE) >
               (utcTime + offsets[current].getOffset())) {
            --current;
        }
        while (offsets[current].getValidityEnd().durationFrom(TAI_REFERENCE) <=
               (utcTime + offsets[current].getOffset())) {
            ++current;
        }
        this.last = current;
        return current;
    }
}
